# -*- coding: utf-8 -*-
import re
from helpers import EntityProcessor, OpenedTagsStat      
        
process_entities = EntityProcessor({
    'amp': '&',
    'mdash': u'—',    
    'ndash': u'–',
    'minus': u'−',
    'hellip': u'…',
    'copy': u'©',
    'trade': u'™',        
})

class HtmlElem(object):
    """
    Base entity for html elements: text, comment, tag.
    """
    @property
    def is_text(self):
        return self.__class__ == HtmlText
    
    @property
    def is_tag(self):
        return self.__class__ == HtmlTag
    
    @property
    def is_comment(self):
        return self.__class__ == HtmlComment
    
    def __repr__(self):
        return "%s('%s')" % (self.__class__.__name__, str(self))
    
    def __unicode__(self):
        return self.__str__    
    
    
class HtmlText(HtmlElem):
    """
    Wrapper for html text.
    """   
        
    def __init__(self, text, cdata=False):                
        if text != '' and text.strip() == '':
            self.text = u' '
        elif cdata:
            self.text = text
        else:
            self.text = process_entities(text) 
            
    def __str__(self):
        return self.text
    
    def __eq__(self, other):
        if isinstance(other, self.__class__):
            return self.text == other.text
        
        if isinstance(other, basestring):
            return self.text == other
        
        return False


class HtmlComment(HtmlText):
    """
    Wrapper for html comment.
    """   
    def __init__(self, text):
        self.text = text


class HtmlTag(HtmlElem):
    """
    Wrapper for html tag.
    """
    
    SINGLE_TAGS = (
        'br',
        'hr', 
        'img', 
        'link',
        'meta',
        '!doctype', 
    )

    _PARSE_ATTRS_RULES = (
        ('"', re.compile('(\w+)\s*=\s*"([^"]*)"')),
        ('\'', re.compile("(\w+)\s*=\s*'([^']*)'")),
        ('', re.compile("(\w+)\s*=\s*([^'\"\s]+)")),
        #('', re.compile("([^\s\"'=])()(?=\s|$)")),  # TODO single attrs
    )
    
    def __init__(self, content):
        if content[0] != '<' or content[-1] != '>':
            raise Exception('Invalid tag `%s`' % content)
        
        content = content[1:-1]
        if content.startswith('/'):  # close tag
            content = content[1:]
            self.is_closed = True
            self.is_pair = True        
        elif content.endswith('/'):  # non-pair tag
            self.is_closed = True
            self.is_pair = False
        else:  # open tag
            self.is_closed = False
            self.is_pair = None  # get it from name later 
                            
        bits = filter(None, content.strip().split(None, 1))
        self.name = bits[0].lower()        

        if self.is_pair is None:
            self.is_pair = not self.name in self.SINGLE_TAGS        
                
        self.attrs = {}            
        if len(bits) == 2:  # has attributes
            str_attrs = bits[1]
            for strip, pattern in self._PARSE_ATTRS_RULES:
                for m in pattern.finditer(str_attrs):
                    self.attrs[m.group(1)] = m.group(2).strip(strip)
#        print self.attrs
        
    def __eq__(self, other):
        if isinstance(other, basestring):
            try:
                return self == self.__class__(other)
            except:
                return False
        elif isinstance(other, HtmlTag):
            return (self.name == other.name and
                    self.is_closed == other.is_closed and
                    self.is_pair == other.is_pair and
                    self.attrs == other.attrs)
        else:
            return False

    
    def __str__(self):
        attrs = ''.join((u' %s="%s"' % (k, v)) for k, v in self.attrs.items())        
        if self.is_pair:
            if self.is_closed:        
                return u'</%s>' % self.name
            else:
                return u'<%s%s>' % (self.name, attrs)
        else:
            return u'<%s%s />' % (self.name, attrs)


_LCOMMENT, _RCOMMENT = '<!--', '-->'
_LCDATA, _RCDATA = '<![CDATA[', ']]>'
def htmlchunks(html, return_comments=False):
    """
    Generator. Returns string or HtmlTag object
    """    

    if not isinstance(html, unicode):
        try:
            html = unicode(html)
        except UnicodeDecodeError:
            raise Exception, '`html` arg of parsehtml must be unicode'
    
    curpos = 0
    size = len(html)
    
    while curpos < size:        
        
        starttag = html.find('<', curpos)
        if starttag == -1:
            text = html[curpos:]
            if text: yield HtmlText(text)            
            break
        
        begin_lcomment = starttag
        end_lcomment = begin_lcomment + len(_LCOMMENT)
        if html[begin_lcomment:end_lcomment] == _LCOMMENT:            
            begin_rcomment = html.find(_RCOMMENT, end_lcomment)
            if begin_rcomment == -1:
                text = html[curpos:].strip()
                if text: yield HtmlText(text)            
                break
            
            text = html[curpos:begin_lcomment]
            if text: yield HtmlText(text)            
             
            if return_comments:            
                yield HtmlComment(html[end_lcomment:begin_rcomment])
                
            curpos = begin_rcomment + len(_RCOMMENT)
            continue

        begin_lcdata = starttag
        end_lcdata = begin_lcdata + len(_LCDATA)
        if html[begin_lcdata:end_lcdata] == _LCDATA:            
            begin_rcdata = html.find(_RCDATA, end_lcdata)
            if begin_rcdata == -1:
                text = html[curpos:].strip()
                if text: yield HtmlText(text)            
                break
            
            text = process_entities(html[curpos:begin_lcdata]) + html[end_lcdata:begin_rcdata]
            yield HtmlText(text, cdata=True)
                
            curpos = begin_rcdata + len(_RCDATA)
            continue
        
        stoptag = html.find('>', starttag)
        if stoptag == -1:
            text = html[curpos:]
            if text: yield HtmlText(text)            
            break
        
        # TODO check for incorrect '<' and '>' and re-search tag begin/end
        tag = HtmlTag(html[starttag:stoptag + 1])        
        
        text = html[curpos:starttag]        
        if text: yield HtmlText(text)
                      
        yield tag
        
        curpos = stoptag + 1

    raise StopIteration


def parsehtml(s, return_comments=False):
    opened = OpenedTagsStat()
    for elem in htmlchunks(s, return_comments=return_comments):
        if elem.is_tag and elem.is_pair: 
            if elem.is_closed:
                opened.drop_last(elem)
            else:
                opened.append(elem)           
        yield elem, opened
    raise StopIteration        
